Skip to content

feat(qwp): support for PEM CA files - #75

Merged
jerrinot merged 2 commits into
mainfrom
jh_pem_support
Jul 29, 2026
Merged

feat(qwp): support for PEM CA files#75
jerrinot merged 2 commits into
mainfrom
jh_pem_support

Conversation

@jerrinot

Copy link
Copy Markdown
Contributor

tls_roots now accepts a PEM certificate file:

QuestDB.connect("wss::addr=db.example.com:9000;tls_roots=/etc/ssl/ca.pem;")

The file may contain one certificate or a bundle, and no longer has to be imported into a password-protected JKS with keytool first.

Existing JKS/PKCS#12 trust stores keep working as before by supplying tls_roots_password, and PKCS#12 now also loads on Java 8.

tls_roots now accepts a PEM certificate file:

QuestDB.connect("wss::addr=db.example.com:9000;tls_roots=/etc/ssl/ca.pem;")

The file may contain one certificate or a bundle, and no longer has to
be imported into a password-protected JKS with keytool first.

Existing JKS/PKCS#12 trust stores keep working as before by supplying
tls_roots_password, and PKCS#12 now also loads on Java 8.
@jerrinot jerrinot added enhancement New feature or request security labels Jul 28, 2026
@jerrinot jerrinot changed the title feat(tls): support for PEM CA files feat(qwp): support for PEM CA files Jul 28, 2026
Reject tls_roots combined with tls_verify=unsafe_off across Sender and QWP config facades. Enforce the invariant in fluent and low-level TLS construction paths, with regression coverage for PEM and password-protected trust stores.
@jerrinot

Copy link
Copy Markdown
Contributor Author

opus-4.8 review:

Verification complete. getEnum restricts tls_verify to exactly {on, unsafe_off} (case-sensitive), so the only reachable insecure spelling is unsafe_off — the string-based static guards cover it, and two mode-based backstops (ClientTlsConfiguration ctor + TlsTrustStore.createSslEngine) hard-fail on roots+insecure regardless. I traced every callsite; the security model is airtight.


Review: PR #75 feat(qwp): support for PEM CA files — Level 3

Verdict: approve. This is a clean, defense-in-depth security PR. 0 Critical, 0 Moderate, 2 Minor. 1 draft concern verified as a non-issue. No committed binaries (the .pem/.crt are text fixtures). All findings in-diff.

What the change does

tls_roots now accepts a raw PEM cert/bundle when no password is given; a password still selects a Java trust store, now with JKS-vs-PKCS12 autodetection (first byte 0x30 → PKCS12). SSLEngine construction is de-duplicated from DelegatingTlsChannel + JavaTlsClientSocket into new TlsTrustStore. A new rule rejects tls_roots + tls_verify=unsafe_off.

Security verification (the reason this is level 3)

The "roots must not combine with disabled validation" invariant holds on every path — verified by reading each callsite, not the diff alone:

Path Guard Basis
ILP config fromConfig line 3580 mode
ILP validateParameters line 3981 mode
WS static validateWsConfig line 3842 string (only unsafe_off reachable)
QWP static validateConfig line 548 string (only unsafe_off reachable)
ClientTlsConfiguration ctor line 44 mode — backstop
TlsTrustStore.createSslEngine line 638 boolean — runtime backstop
Programmatic ILP AdvancedTlsSettings order-independent cross-guards
Programmatic QWP withTls/withInsecureTls clear trustStorePath (1235/1293), "last wins"

All three transports (tcps→DelegatingTlsChannel, https/wss→JavaTlsClientSocket) route through TlsTrustStore; PEM path keeps endpointIdentificationAlgorithm=https hostname validation. No combination reaches an insecure socket with roots silently dropped — worst case is a build-time exception.

Minor

  1. PR scope understated. Title says feat(qwp): but the change equally touches ILP TCP (DelegatingTlsChannel) and ILP HTTP (JavaTlsClientSocket). The first commit already uses the accurate feat(tls):. Retitle to feat(tls): so the squash message reflects the real surface.
  2. Inconsistent null-arg messages (nit). QwpQueryClient.withTrustStore(String) throws "pemRootsPath must not be null" while AdvancedTlsSettings.customTrustStore(String) null/blank throws "trust store path cannot be empty nor null". Harmless, just inconsistent wording.

Downgraded (checked, not a bug)

  • "String-based unsafe_off guard in the static validators could be bypassed by another spelling."ConfigView.getEnum (ConfigView.java:140-146) rejects any tls_verify value outside {on, unsafe_off}, case-sensitively, so unsafe_off is the only insecure spelling that reaches the guard, and the mode/boolean backstops catch it even if a static validator ever missed.

Coverage

Solid: TlsTrustStoreTest covers PEM single/bundle/classpath (real checkServerTrusted chain validation), empty-PEM rejection, JKS-without-password → helpful error, and JKS+PKCS12 still-supported. Config-string tests cover the new reject rules on ILP, WS, and QWP. Resources cleaned in finally. assertMemoryLeak not needed (JDK crypto, no native alloc).

Draft findings: 3 raised, 2 minor confirmed, 1 dropped as false positive. Split: 2 in-diff, 0 out-of-diff. Out-of-diff is legitimately zero here — TlsTrustStore is new and its only two callers are both in the diff; the cross-context pass walked all createSslEngine/ClientTlsConfiguration construction sites and found them consistent.

@mtopolnik

Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 86 / 100 (86.00%)

file detail

path covered line new line coverage
🔵 io/questdb/client/cutlass/line/tcp/DelegatingTlsChannel.java 0 1 00.00%
🔵 io/questdb/client/network/TlsTrustStore.java 47 58 81.03%
🔵 io/questdb/client/cutlass/qwp/client/QwpQueryClient.java 14 15 93.33%
🔵 io/questdb/client/Sender.java 18 19 94.74%
🔵 io/questdb/client/ClientTlsConfiguration.java 2 2 100.00%
🔵 io/questdb/client/network/JavaTlsClientSocket.java 5 5 100.00%

@jerrinot
jerrinot merged commit 9ccfadb into main Jul 29, 2026
15 checks passed
@jerrinot
jerrinot deleted the jh_pem_support branch July 29, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants